package eda.main;

import java.util.Random;
import java.util.Scanner;

import eda.hashTable.HashTable;
import eda.hashTable.HashTableImpl;
import eda.util.ADTNoSuchElement;
import eda.util.ADTOverflowException;
import eda.util.ADTUnderflowException;
import eda.util.Constants;

public class Main {
	
	public static final String LOGO = 
"\n|||   ||| |||||||||   ||||||| |||   ||| \n" +
  "|||   ||| |||   ||| |||       |||   ||| \n"+
  "||||||||| |||||||||   ||||||  ||||||||| \n"+
  "|||   ||| |||   |||       ||| |||   ||| \n"+
  "|||   ||| |||   ||| ||||||||  |||   ||| \n"+
"\n"+
"\n||||||||| ||||||||| |||||||   |||       ||||||||| \n"+
"   |||    |||   ||| |||   ||| |||       ||| \n"+
"   |||    ||||||||| ||||||    |||       |||||| \n"+
"   |||    |||   ||| |||   ||| |||       ||| \n"+
"   |||    |||   ||| ||||||    ||||||||| |||||||||\n";
	
	public static final int CLOSED_ADDRESSING = 1;
	public static final int OPEN_ADDRESSING = 2;

	public static final int LINEAR_PROBE = 1;
	public static final int QUADRATIC_PROBE = 2;
	public static final int DOUBLE_HASHING = 3;

	public static final int INTEGER = 1;
	public static final int DOUBLE = 2;
	public static final int FLOAT = 3;
	public static final int STRING = 4;

	public static final int INSERT = 1;
	public static final int SEARCH = 2;
	public static final int REMOVE = 3;
	public static final int STATISTICS = 4;
	public static final int AUTOMATIC_TASKS = 5;
	public static final int CONFIGURE = 6;
	public static final int EXIT = 7;
	
	public static final int AUTO_INSERT = 1;
	public static final int AUTO_SEARCH = 2;
	public static final int AUTO_REMOVE = 3;
	public static final int RETURN = 4;
	
	public static int chosenAddressing;
	public static int chosenProbe;
	public static int chosenKeyClass;
	public static int chosenValueClass;

	public static final String INTRO = 
		"(HASH TABLE IMPLEMENTATIONS)\nProjeto de Laboratorio de Estrutura "
		+ "de Dados e Algoritmos\nProf.: Adalberto Cajueiro\n"
		+ "Grupo: Laercio Vitorino, Tarciso Braz, Manoel Martiniano,\n"
		+ "       Pedro Rawan, Jose Arthur e Guilherme Santos\n\n";
	public static final String FINAL = "\nVoce finalizou o software.\nObrigado!\n";
	
	public static final String PROMPT = "\t>: ";
	public static final String CONFIRMATION = "\n\tOK?\n\t>: ";
	public static final String INVALID_OPTION_WARNING ="- Opcao invalida!\n\n";
	public static final String INVALID_STRING_ENTRY = "- Entrada invalida!\n\n";
	
	public static final String SETTING_UP_HASH_TABLE =
			"\n<DEFINA AS CONFIGURACOES DA TABELA HASH>\n\n";
	public static final String TESTING_HASH_TABLE =
			"\n<TESTANDO A TABELA HASH CONFIGURADA>\n\n";
	
	public static final String KIND_OF_ADDRESSING_QUESTION =
			"- Que tipo de enderecamento?\n"
			+ "\t1 - Fechado\n" + "\t2 - Aberto\n";
	public static final String KIND_OF_PROBE_QUESTION =
			"- Qual o tipo de probe?\n"
			+ "\t1 - Linear Probe\n"
			+ "\t2 - Quadratic Probe\n"
			+ "\t3 - Double Hashing\n";
	public static final String KIND_OF_KEY_CLASS_QUESTION =
			"- Qual a classe desejada para as chaves?\n"
			+ "\t1 - Integer\n"
			+ "\t2 - Double\n"
			+ "\t3 - Float\n"
			+ "\t4 - String\n";
	public static final String KIND_OF_VALUE_CLASS_QUESTION =
			"- Qual a classe desejada para os valores?\n"
			+ "\t1 - Integer\n"
			+ "\t2 - Double\n"
			+ "\t3 - Float\n"
			+ "\t4 - String\n";
	public static final String KIND_OF_ACTION_QUESTION =
			"- O que voce deseja fazer?\n" +
			"\t1 - Inserir\n" +
			"\t2 - Pesquisar\n" +
			"\t3 - Remover\n" +
			"\t4 - Ver estatisticas\n" + //Pegar o tipo de classe pras key e pros values; Pegar o size, length, porcentagem de ocupacao, quanto falta pro rehash, e dizer a funcao de hash
			"\t5 - Tarefas automaticas\n" +
			"\t6 - Mudar as configuracoes da Tabela Hash (lhe entrega outra tabela)\n" +
			"\t7 - Sair do software\n";
	public static final String KIND_OF_AUTOMATIC_TASK_QUESTION =
			"- Que tarefa automatica voce deseja executar?\n" +
			"\t1 - Inserir n elementos randomicamente\n" +
			"\t2 - Procurar por n elementos dentro de um intervalo\n" +
			"\t3 - Remover n elementos dentro de um intervalo\n" +
			"\t4 - Voltar\n";

	public static final String NUMBER_OF_KEY = "- Qual o numero da chave?\n";
	public static final String STRING_KEY_REQUEST = "- Qual a string que vai servir de chave?\n";
	public static final String NUMBER_OF_VALUE = "- Qual o numero do valor?\n";
	public static final String STRING_VALUE_REQUEST = "- Qual a string que vai servir de valor?\n";
	public static String SHOW_TABLE = "- Sua tabela se encontra assim:\n\n";
	public static String SHOW_VALUE = "- O valor correspondente a chave passada eh: ";
	public static final String HOW_MANY = "- Quantos?\n";
	public static final String FROM = "- De onde?\n";
	public static final String TO = "- Ate onde?\n";
	
	public static final String STRUCTURE_IS_FULL = "- A tabela estah cheia!\n";
	public static final String STRUCTURE_IS_EMPTY = "- A tabela estah vazia!\n";
	public static final String THE_REQUIRED_KEY_DOES_NOT_EXISTS = "- O elemento desejado nao existe na tabela!\n";

	
	@SuppressWarnings({ "unchecked" })
	public static HashTable currentHashTable;
	public static Scanner reader = new Scanner(System.in);
	public static MainUtil util = new MainUtil();
	public static boolean exit = false;
	public static boolean testing = false;
	
	@SuppressWarnings({ "unchecked" })
	public static void main(String args[]) {
		
		System.out.println(LOGO);
		System.out.print(INTRO);

		util.setTableLength(Constants.INITIAL_SIZE_OF_STRUCTURE);
		
		while (!exit) {
			
			System.out.print(SETTING_UP_HASH_TABLE);
			
			chosenAddressing = askSomethingAndRetrieveAnswer(
					KIND_OF_ADDRESSING_QUESTION + PROMPT, CLOSED_ADDRESSING,
					OPEN_ADDRESSING, INVALID_OPTION_WARNING);
			if (chosenAddressing == OPEN_ADDRESSING) {
				chosenProbe = askSomethingAndRetrieveAnswer(KIND_OF_PROBE_QUESTION
						+ PROMPT, LINEAR_PROBE, DOUBLE_HASHING,
						INVALID_OPTION_WARNING);
				chosenProbe = chosenProbe - 2;
			}

			chosenKeyClass = askSomethingAndRetrieveAnswer(
					KIND_OF_KEY_CLASS_QUESTION + PROMPT, INTEGER, STRING,
					INVALID_OPTION_WARNING);

			chosenValueClass = askSomethingAndRetrieveAnswer(
					KIND_OF_VALUE_CLASS_QUESTION + PROMPT, INTEGER, STRING,
					INVALID_OPTION_WARNING);

			Class[] parameterTypes = null;
			Object[] parameters = null;
			switch (chosenAddressing) {
			case CLOSED_ADDRESSING: {
				parameterTypes = null;
				parameters = null;
				break;
			}
			case OPEN_ADDRESSING: {
				parameterTypes = new Class[] {Integer.class};
				parameters = new Integer[]{new Integer(chosenProbe)};
			}
			}
			try{
				currentHashTable = HashTableImpl.class.getConstructor(parameterTypes).newInstance(parameters);
				testing = true;
			}
			catch(Exception weirdStuffHappenedHere){
				weirdStuffHappenedHere.printStackTrace();
			}
			
			System.out.print(TESTING_HASH_TABLE);

			ElementAtributteInfo initKey = getElementAtributteInitSample(chosenKeyClass);
			ElementAtributteInfo initValue = getElementAtributteInitSample(chosenValueClass);

			util.setKeyClass(initKey.elementAtributte.getClass());
			util.setValueClass(initValue.elementAtributte.getClass());
			
			while(testing){
				
				int chosenAction = askSomethingAndRetrieveAnswer(KIND_OF_ACTION_QUESTION+PROMPT,
						INSERT, EXIT, INVALID_OPTION_WARNING);
				
				ElementAtributteInfo key;
				ElementAtributteInfo value;
				
				switch(chosenAction){
				case INSERT:{
					
					key = requestElementAtributteSample(chosenKeyClass, NUMBER_OF_KEY, STRING_KEY_REQUEST);
					value = requestElementAtributteSample(chosenValueClass, NUMBER_OF_VALUE, STRING_VALUE_REQUEST);
					try{
						double before = System.currentTimeMillis()/1000.0;
						currentHashTable.insert(key.elementAtributte, value.elementAtributte);
						System.out.println("\t Inserido com sucesso em "+((System.currentTimeMillis()/1000.0)-before)+" s\n");
					}
					catch (ADTOverflowException overflowHappened){
						System.out.print(STRUCTURE_IS_FULL);
						break;
					}
					catch(Exception somethingWeirdHappenedHereDude){
						somethingWeirdHappenedHereDude.printStackTrace();
					}
					
					int tableSize = currentHashTable.size();
					
					if ( tableSize > 0.75*util.getTableLength() ){
						util.setTableLength(Constants.INCREASING_FACTOR+util.getTableLength());
					}
					if ( util.getTableLength() > Constants.MAX_SIZE_OF_STRUCTURE){
						util.setTableLength(Constants.MAX_SIZE_OF_STRUCTURE);
					}
					
					System.out.print(SHOW_TABLE+currentHashTable+CONFIRMATION);
					reader.nextLine();
					break;
				}
				case SEARCH:{

					key = requestElementAtributteSample(chosenKeyClass, NUMBER_OF_KEY, STRING_KEY_REQUEST);
					if ( !currentHashTable.containsKey(key.elementAtributte) ){
						System.out.print(THE_REQUIRED_KEY_DOES_NOT_EXISTS);
						break;
					}

					double before = System.currentTimeMillis()/1000.0;
					System.out.print(SHOW_VALUE+currentHashTable.search(key.elementAtributte)+" encontrado em "+((System.currentTimeMillis()/1000.0)-before)+" s\n"+CONFIRMATION);
					reader.nextLine();
					break;
				}
				case REMOVE:{
					
					key = requestElementAtributteSample(chosenKeyClass, NUMBER_OF_KEY, STRING_KEY_REQUEST);
					try{						
						double before = System.currentTimeMillis()/1000.0;
						currentHashTable.remove(key.elementAtributte);
						System.out.println("\t Removido com sucesso em "+((System.currentTimeMillis()/1000.0)-before)+" s\n");
					}
					catch (ADTUnderflowException underflowHappened){
						System.out.print(STRUCTURE_IS_EMPTY);
						break;
					}
					catch (ADTNoSuchElement elementDontExists){
						System.out.print(THE_REQUIRED_KEY_DOES_NOT_EXISTS);
						break;
					}
					catch(Exception somethingWeirdHappenedHereDude){
						somethingWeirdHappenedHereDude.printStackTrace();
					}

					System.out.print(SHOW_TABLE+currentHashTable+CONFIRMATION);
					reader.nextLine();					
					break;
				}
				case STATISTICS:{

					System.out.print(SHOW_TABLE+currentHashTable+"\n\n");
					System.out.print("\n- Estatisticas:\n" +
							"\tTipo de Enderecamento: " + (chosenAddressing==1?"Fechado":
							"Aberto\n\tTipo de Probe: "+(chosenProbe == -1?"Linear":(chosenProbe == 0?"Quadratic":"Double Hashing"))) +"\n" +
							"\tTipo das Chaves: "+util.getKeyClass().getCanonicalName()+"\n" +
							"\tTipo dos Valores: "+util.getValueClass().getCanonicalName()+"\n" +
							"\tQuantidade de Elementos: "+currentHashTable.size()+"\n" +
							"\tQuantidade de Espacos: " +util.getTableLength()+"\n" +
							(chosenAddressing != 1 ? 
							"\tTabela "+String.format("%.2f", 100*((float)currentHashTable.size()/(float)util.getTableLength()))+"% ocupada\n\n" :
							"\n"));
					break;
					
				}
				case CONFIGURE:{
					util.setTableLength(Constants.INITIAL_SIZE_OF_STRUCTURE);
					testing = false;
					currentHashTable = null;
					break;
					
				}
				case AUTOMATIC_TASKS:{
					
					int chosenAutomaticTask = askSomethingAndRetrieveAnswer(
							KIND_OF_AUTOMATIC_TASK_QUESTION+PROMPT, AUTO_INSERT,
							RETURN, INVALID_OPTION_WARNING);

					switch (chosenAutomaticTask) {
					case AUTO_INSERT: {

						int howMany = askSomethingAndRetrieveAnswer(HOW_MANY+PROMPT,
								Integer.MIN_VALUE, Integer.MAX_VALUE,
								INVALID_OPTION_WARNING);
						
						System.out.print("- Inserindo "+howMany+" elementos randomicamente\n\n");

						double before = System.currentTimeMillis()/1000.0;
						
						for ( int i=0; i<howMany; i++ ){
							
							key = getElementAtributteRandomSample(chosenKeyClass);
							value = getElementAtributteRandomSample(chosenValueClass);
							
							System.out.print("- Inserindo o "+(i+1)+"o. elemento de chave "+key.elementAtributte+" e valor "+value.elementAtributte+"\n");
							
							try{
								currentHashTable.insert(key.elementAtributte, value.elementAtributte);
								System.out.println("\t Inserido com sucesso em "+((System.currentTimeMillis()/1000.0)-before)+" s\n");
							}
							catch (ADTOverflowException overflowHappened){
								System.out.print(STRUCTURE_IS_FULL);
								break;
							}
							
							int tableSize = currentHashTable.size();
							
							if ( tableSize > 0.75*util.getTableLength() ){
								util.setTableLength(Constants.INCREASING_FACTOR+util.getTableLength());
							}
							if ( util.getTableLength() > Constants.MAX_SIZE_OF_STRUCTURE){
								util.setTableLength(Constants.MAX_SIZE_OF_STRUCTURE);
							}
							
						}
						
						
						System.out.print("\n- Tarefa automatica finalizada\n\n");						
						break;
					}
					case AUTO_SEARCH: {

						if ( !(util.getKeyClass().getSimpleName().equals("String")) ){
							
							System.out.println("- Entre com o intervalo de hashCodes: \n");
							int minLimit = askSomethingAndRetrieveAnswer(FROM
									+ PROMPT, Integer.MIN_VALUE, Integer.MAX_VALUE,
									INVALID_OPTION_WARNING);
							int maxLimit = askSomethingAndRetrieveAnswer(TO
									+ PROMPT, minLimit, Integer.MAX_VALUE,
									INVALID_OPTION_WARNING);
							
							System.out.print("- Pesquisando por chaves dentro do intervalo fechado ["+minLimit+","+maxLimit+"]\n\n");
							
							double before = System.currentTimeMillis()/1000.0;

							for ( int i=minLimit; i<=maxLimit; i++ ){
								
								key = getSpecificElementAtributteSample(chosenKeyClass, i);
								if ( currentHashTable.containsKey(key.elementAtributte) ){
									System.out.println("\tChave: "+key.elementAtributte+"|Valor: "+currentHashTable.search(key.elementAtributte)+" encontrado em "+((System.currentTimeMillis()/1000.0)-before)+" s\n");
								}
								
							}
							System.out.print("\n- Tarefa automatica finalizada\n\n");

						}
						else{
							
							System.out.print("\n- Essa funcao nao eh suportada para chaves do tipo "+util.getKeyClass()+"\n\n");
							
						}
						
						
						break;
					}
					case AUTO_REMOVE: {

						if ( !(util.getKeyClass().getSimpleName().equals("String")) ){
							
							System.out.println("- Entre com o intervalo de hashCodes: \n");
							
							int minLimit = askSomethingAndRetrieveAnswer(FROM
									+ PROMPT, Integer.MIN_VALUE, Integer.MAX_VALUE,
									INVALID_OPTION_WARNING);
							int maxLimit = askSomethingAndRetrieveAnswer(TO
									+ PROMPT, minLimit, Integer.MAX_VALUE,
									INVALID_OPTION_WARNING);
							
							System.out.print("- Removendo chaves dentro do intervalo fechado ["+minLimit+","+maxLimit+"]\n\n");
							
							long before = System.currentTimeMillis()/1000;
							
							for ( int i=minLimit; i<=maxLimit; i++ ){
								
								key = getSpecificElementAtributteSample(chosenKeyClass, i);
								try{
									currentHashTable.remove(key.elementAtributte);
									System.out.println("\tElemento de chave "+key.elementAtributte+" removido em "+((System.currentTimeMillis()/1000)-before)+" s\t");
								}
								catch(ADTUnderflowException underflowHappened){
									System.out.print(STRUCTURE_IS_EMPTY);
									break;
								}
								catch(ADTNoSuchElement shouldntHappen){
								}
								catch(Exception shouldntHappen){
									shouldntHappen.printStackTrace();
								}
								
							}
							System.out.print("\n- Tarefa automatica finalizada\n\n");
							
						}
						else{
							
							System.out.print("\n- Essa funcao nao eh suportada para chaves do tipo "+util.getKeyClass()+"\n\n");
							
						}
						
						break;
					}
					case RETURN: {
						break;
					}
					}

					break;
				}
				case EXIT:{
					
					testing = false;
					exit = true;
					break;
					
				}
				}
			}
			
		}
		
		System.out.print(FINAL);
		
	}

	public static int askSomethingAndRetrieveAnswer(String question,
			int minLimit, int maxLimit, String warning) {
		int choice = 0;
		while (choice == 0) {
			System.out.print(question);
			if (reader.hasNextLine()) {
				String entry = reader.nextLine();
				try {
					choice = Integer.parseInt(entry);
					if (choice < minLimit || choice > maxLimit) {
						throw new Exception();
					}
					break;
				} catch (Exception anythingElseThanIntegers) {
					System.out.print(warning);
					choice = 0;
				}
			}
		}
		return choice;
	}

	public static double askSomethingAndRetrieveDoubleAnswer(String question,
			int minLimit, int maxLimit, String warning) {
		double choice = 0;
		while (choice == 0) {
			System.out.print(question);
			if (reader.hasNextLine()) {
				String entry = reader.nextLine();
				try {
					choice = Double.parseDouble(entry);
					if (choice < minLimit || choice > maxLimit) {
						throw new Exception();
					}
					break;
				} catch (Exception anythingElseThanIntegers) {
					System.out.print(warning);
					choice = 0;
				}
			}
		}
		return choice;
	}

	public static float askSomethingAndRetrieveFloatAnswer(String question,
			int minLimit, int maxLimit, String warning) {
		float choice = 0;
		while (choice == 0) {
			System.out.print(question);
			if (reader.hasNextLine()) {
				String entry = reader.nextLine();
				try {
					choice = Float.parseFloat(entry);
					if (choice < minLimit || choice > maxLimit) {
						throw new Exception();
					}
					break;
				} catch (Exception anythingElseThanIntegers) {
					System.out.print(warning);
					choice = 0;
				}
			}
		}
		return choice;
	}
	
	public static String askForString(String question, String warning){
		boolean approved = false;
		String entry = "";
		while (!approved) {
			System.out.print(question);
			if (reader.hasNextLine()) {
				entry = reader.nextLine();
				try {
					if (entry.trim().isEmpty()) {
						throw new Exception();
					}
					approved = true;
					break;
				} catch (Exception anythingElseThanString) {
					System.out.print(warning);
				}
			}
		}
		return entry;
	}
	
	@SuppressWarnings("unchecked")
	public static ElementAtributteInfo requestElementAtributteSample(int chosen, String intToBeReturnedQuestion, String stringToBeReturnedQuestion){
		
		ElementAtributteInfo toReturn = null;
		
		switch(chosen){
		case INTEGER:{
			
			int number = Main.askSomethingAndRetrieveAnswer(intToBeReturnedQuestion+PROMPT,
					Integer.MIN_VALUE, Integer.MAX_VALUE,
					INVALID_OPTION_WARNING);
			toReturn = new ElementAtributteInfo<Integer>(number);
			break;
			
		}
		case DOUBLE:{
			
			double number = Main.askSomethingAndRetrieveDoubleAnswer(intToBeReturnedQuestion+PROMPT,
					Integer.MIN_VALUE, Integer.MAX_VALUE, INVALID_OPTION_WARNING);
			toReturn = new ElementAtributteInfo<Double>(number);
			break;
			
		}
		case FLOAT:{
			
			float number = Main.askSomethingAndRetrieveFloatAnswer(intToBeReturnedQuestion+PROMPT,
					Integer.MIN_VALUE, Integer.MAX_VALUE, INVALID_OPTION_WARNING);
			toReturn = new ElementAtributteInfo<Float>(number);
			break;
			
		}
		case STRING:{
			
			String string = Main.askForString(stringToBeReturnedQuestion+PROMPT, INVALID_STRING_ENTRY);
			toReturn = new ElementAtributteInfo<String>(string);
			break;
			
		}
		}
		return toReturn;
		
	}
	
	@SuppressWarnings("unchecked")
	public static ElementAtributteInfo getElementAtributteInitSample(int chosen){
		
		ElementAtributteInfo toReturn = null;
		
		switch(chosen){
		case INTEGER:{
			toReturn = new ElementAtributteInfo<Integer>(0);
			break;
		}
		case DOUBLE:{
			toReturn = new ElementAtributteInfo<Double>(0.);
			break;
		}
		case FLOAT:{
			toReturn = new ElementAtributteInfo<Float>(0.0f);
			break;
		}
		case STRING:{
			toReturn = new ElementAtributteInfo<String>("");
			break;
		}
		}
		
		return toReturn;
		
	}
	
	@SuppressWarnings("unchecked")
	public static ElementAtributteInfo getSpecificElementAtributteSample(int chosen, int hashCode){
		
		ElementAtributteInfo toReturn = null;
		
		switch(chosen){
		case INTEGER:{
			toReturn = new ElementAtributteInfo<Integer>(hashCode);
			break;
		}
		case DOUBLE:{
			toReturn = new ElementAtributteInfo<Double>((double)hashCode);
			break;
		}
		case FLOAT:{
			toReturn = new ElementAtributteInfo<Float>((float)hashCode);
			break;
		}
		case STRING:{
			
			util.setStringHashCode(hashCode);
			
			Object objStub = new Object(){
				public int hashCode(){
					return util.getStringHashCode();
				}
			};
			toReturn = new ElementAtributteInfo<String>((String)objStub);
			break;
			
		}
		}
		
		return toReturn;
		
	}
	
	@SuppressWarnings("unchecked")
	public static ElementAtributteInfo getElementAtributteRandomSample(int chosen){
		
		Random random = new Random();
		ElementAtributteInfo toReturn = null;
		
		switch(chosen){
		case INTEGER:{
			toReturn = new ElementAtributteInfo<Integer>(random.nextInt());
			break;
		}
		case DOUBLE:{
			toReturn = new ElementAtributteInfo<Double>(random.nextDouble());
			break;
		}
		case FLOAT:{
			toReturn = new ElementAtributteInfo<Float>(random.nextFloat());
			break;
		}
		case STRING:{
			
			StringBuilder builder = new StringBuilder();
			double a = random.nextDouble();
			double b = random.nextDouble();
			int letters = Math.abs((int) ( (a >= b ? a/b : b/a ) ) );
			for ( int i=0; i<letters; i++ ){
				builder.append((char)(random.nextInt()));
			}
			toReturn = new ElementAtributteInfo<String>(builder.toString());
			break;
		}
		}
		
		return toReturn;
		
	}
	
}